﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Web;
using System.Web.Profile;
using System.Web.Security;
using System.Web.UI;
using ScrewTurn.Wiki.PluginFramework;

namespace Db4objects.ScrewTurnWiki.SystemMembership {
	public class SystemWebMembershipProxyProvider : IUsersStorageProviderV30 {
		static readonly ComponentInformation info;
		static readonly string helpHtml;

		static SystemWebMembershipProxyProvider() {
			var thisType = typeof(SystemWebMembershipProxyProvider);
			info = new ComponentInformation("System.Web.Membership Proxying User Provider",
				"Versant",
				thisType.Assembly.GetName().Version.ToString(),
				"http://www.versant.com",
				null);

			//var resources = new ResourceManager(thisType);
			//helpHtml = resources.GetString("HtmlHelp.htm");
			//thisType, "HtmlHelp.htm"
			using (var reader = new StreamReader(thisType.Assembly.GetManifestResourceStream("Db4objects.ScrewTurnWiki.SystemMembership.HelpHtml.htm"))) {
				helpHtml = reader.ReadToEnd();
			}
		}

		#region IProviderV30 Members
		/// <summary>
		/// Initializes the Storage Provider
		/// </summary>
		/// <param name="host"></param>
		/// <param name="config">configuration data, if any</param>
		public void Init(IHostV30 host, string config) {
			if (host == null) throw new ArgumentNullException("host");
			if (config == null) throw new ArgumentNullException("config");

			var membershipProvider = Membership.Provider;
			if (membershipProvider.RequiresQuestionAndAnswer)
				throw new InvalidConfigurationException(this.Information.Name + " does not support the RequiresQuestionAndAnswer option on the current Membership Provider.");

			// For UserInfo
			VerifyProfileField("DisplayName");
			// For User Profile
			VerifyProfileField("Culture");
			VerifyProfileField("Timezone");
			// For Email Notifications
			VerifyProfileField("PageChanges");
			VerifyProfileField("DiscussionMessages");
			VerifyProfileField("NamespacePageChanges");
			VerifyProfileField("NamespaceDiscussionMessages");
		}

		private void VerifyProfileField(string profileFieldName) {
			var profileProperties = ProfileBase.Properties;
			var profileProperty = profileProperties[profileFieldName];
			if (profileProperty == null)
				throw new InvalidConfigurationException(this.Information.Name + " requires a '" + profileFieldName + "' profile field defined in the current Profile Provider.");
			if (profileProperty.PropertyType != typeof(string))
				throw new InvalidConfigurationException(this.Information.Name + " requires that the '" + profileFieldName + "' profile field is configured with type=\"String\".");
			if (profileProperty.IsReadOnly)
				throw new InvalidConfigurationException(this.Information.Name + " requires that the '" + profileFieldName + "' profile field is configured with readOnly=\"true\".");
			//if (!(bool)profileProperty.Attributes["AllowAnonymous"])
			//  throw new InvalidConfigurationException(this.Information.Name + " requires that the '" + profileFieldName + "' profile field is configured with allowAnonymous=\"true\".");
		}

		/// <summary>
		/// Gets the Information about the Provider.
		/// </summary>
		public ComponentInformation Information {
			get { return info; }
		}

		//cleanup/disconnect
		//not guaranteed to ever be called.
		public void Shutdown() {}

		/// <summary>
		/// Gets a brief summary of the configuration string format.
		/// null if no configuration is needed
		/// </summary>
		public string ConfigHelpHtml {
			get {
				return helpHtml;
			}
		}
		#endregion

		public bool UserAccountsReadOnly {
			get { return false; }
		}

		public bool UserGroupsReadOnly {
			get { return false; }
		}

		public bool UsersDataReadOnly {
			get { return false; }
		}

		/// <summary>
		/// Gets a value indicating whether group membership is read-only.
		/// </summary>
		/// <remarks>
		/// If UserAccountsReadOnly is false, then this property must also be false.
		/// If this property is true, the provider should return membership data compatible with default user groups.
		/// </remarks>
		public bool GroupMembershipReadOnly {
			get { return false; }
		}

		#region Users
		/// <summary>
		/// Adds a new user to the system.
		/// </summary>
		/// <param name="displayName">name to display for the user, can be null</param>
		/// <param name="active">is the account active</param>
		/// <param name="created">account creation datetime</param>
		/// <returns></returns>
		public UserInfo AddUser(string username, string displayName, string password, string email, bool active, DateTime created) {
			if(String.IsNullOrEmpty(username))
				throw new ArgumentNullException("username");
			if (String.IsNullOrEmpty(password))
				throw new ArgumentNullException("password");
			if(String.IsNullOrEmpty(email))
				throw new ArgumentNullException("email");

			MembershipCreateStatus createStatus;
			var membershipUser = Membership.CreateUser(username, password, email, null, null, active, out createStatus);

			switch(createStatus){
				case MembershipCreateStatus.DuplicateEmail:
				case MembershipCreateStatus.DuplicateProviderUserKey:
				case MembershipCreateStatus.DuplicateUserName:
				case MembershipCreateStatus.UserRejected:
					return null;
			}

			var profile = ProfileBase.Create(membershipUser.UserName);
			profile["DisplayName"] = displayName;
			profile.Save();

			return ToUserInfo(membershipUser);
		}

		/// <summary>
		/// get a user account
		/// </summary>
		/// <param name="username"></param>
		/// <returns>null if no matching user was found</returns>
		public UserInfo GetUser(string username) {
			if (String.IsNullOrEmpty(username)) throw new ArgumentNullException("username");

			var membershipUser = Membership.GetUser(username);
			return ToUserInfo(membershipUser);
		}

		/// <summary>
		/// get a user account
		/// </summary>
		/// <param name="email"></param>
		/// <returns>null if no matching user was found</returns>
		public UserInfo GetUserByEmail(string email) {
			if (String.IsNullOrEmpty(email)) throw new ArgumentNullException("email");
			
			var users = Membership.FindUsersByEmail(email).OfType<MembershipUser>();
			return ToUserInfo(users.SingleOrDefault());
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns>All users, sorted by username</returns>
		public UserInfo[] GetUsers() {
			return new UserInfo[] { };
			//HACK: Don't return all users
			//var users = Membership.GetAllUsers().OfType<MembershipUser>().OrderBy(u=>u.UserName);
			//return (from u in users select ToUserInfo(u)).ToArray();
		}

		/// <summary>
		/// Gets all the users that have the specified element in their data.
		/// </summary>
		/// <param name="key">key of the data</param>
		/// <returns>the users and the data</returns>
		public IDictionary<UserInfo, string> GetUsersWithData(string key) {
			if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
			//HACK: don't return any users.
			return new Dictionary<UserInfo, string>();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="user">user to modify</param>
		/// <param name="newDisplayName">new display name (can be null)</param>
		/// <param name="newPassword">new password (null or empty to keep current password)</param>
		/// <param name="newEmail"></param>
		/// <param name="newActive"></param>
		/// <returns>The Modified UserInfo instance</returns>
		public UserInfo ModifyUser(UserInfo user, string newDisplayName, string newPassword, string newEmail, bool newActive) {
			if (user == null) throw new ArgumentNullException("user");
			var membershipUser = Membership.GetUser(user.Username, false);
			if (membershipUser == null)
				throw new ArgumentException("Could not locate user '" + user.Username + "' in Membership Provider.", "user");

			membershipUser.ChangePassword(membershipUser.GetPassword(), newPassword);
			membershipUser.Email = newEmail;
			membershipUser.IsApproved = newActive;
			Membership.UpdateUser(membershipUser);

			var profile = GetProfile(membershipUser);
			profile["DisplayName"] = newDisplayName;
			profile.Save();

			return ToUserInfo(membershipUser);
		}

		/// <returns>true if the user was removed successfuly</returns>
		public bool RemoveUser(UserInfo user) {
			if (user == null) throw new ArgumentNullException("user");

			return Membership.DeleteUser(user.Username);
		}

		#endregion

		#region Login/out
		/// <summary>
		/// Notifies the provider that a user has logged in through the authentication cookie.
		/// </summary>
		/// <param name="user"></param>
		public void NotifyCookieLogin(UserInfo user) {
			if (user == null) throw new ArgumentNullException("user");
			Membership.GetUser(true);
		}

		public void NotifyLogout(UserInfo user) {
			if (user == null) throw new ArgumentNullException("user");
			if(HttpContext.Current == null) return;
			switch (HttpContext.Current.User.Identity.AuthenticationType) {
				case "Forms":
					FormsAuthentication.SignOut();
					break;
			}
		}

		public bool TestAccount(UserInfo user, string password) {
			if (user == null) throw new ArgumentNullException("user");
			return Membership.ValidateUser(user.Username, password);
		}

		public UserInfo TryAutoLogin(System.Web.HttpContext context) {
			if (context == null) throw new ArgumentNullException("context");

			if (HttpContext.Current != null &&
				HttpContext.Current.User.Identity.IsAuthenticated)
				return ToUserInfo(Membership.GetUser(true));
			return null;
		}

		public UserInfo TryManualLogin(string username, string password) {
			if (String.IsNullOrEmpty(username)) throw new ArgumentNullException("username");
			//if (String.IsNullOrEmpty(password)) throw new ArgumentNullException("password");

			if (Membership.ValidateUser(username, password))
				return GetUser(username);
			return null;
		}
		#endregion

		#region UserGroups/Roles
		/// <summary>
		/// Adds a user group (role)
		/// </summary>
		/// <param name="description">ignored by the System Role Providers</param>
		public UserGroup AddUserGroup(string name, string description) {
			if (String.IsNullOrEmpty(name))
				throw new ArgumentNullException("name");
			//if (description == null)
			//  throw new ArgumentNullException("description");

			Roles.CreateRole(name);
			return new MembershipUserGroup(name, String.Empty, this, name);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns>All user groups (Roles), sorted by name</returns>
		public UserGroup[] GetUserGroups() {
			var roles = Roles.GetAllRoles();
			Array.Sort<string>(roles);

			return (from roleName in roles
							orderby roleName
							select new MembershipUserGroup(roleName, String.Empty, this, roleName) 
								{Users=GetUsersForGroup(roleName) }
							).ToArray();
		}

		private string[] GetUsersForGroup(string roleName) {
			if (String.IsNullOrEmpty(roleName)) throw new ArgumentNullException("roleName");
			return new string[] { };
			//HACK: Don't return all users
			//return Roles.GetUsersInRole(roleName);
		}

		public UserGroup ModifyUserGroup(UserGroup group, string description) {
			if (group == null) throw new ArgumentNullException("group");
			//if (description == null) throw new ArgumentNullException("description");
			
			// System Role Provider does not support role descriptions
			return group;
		}

		public bool RemoveUserGroup(UserGroup group) {
			if (group == null) throw new ArgumentNullException("group");

			return Roles.DeleteRole(group.Name);
		}

		public UserInfo SetUserMembership(UserInfo user, string[] groups) {
			if (user == null) throw new ArgumentNullException("user");

			var currentRoles = Roles.GetRolesForUser(user.Username);
			Roles.RemoveUserFromRoles(user.Username,
				(from r in currentRoles
				 where !groups.Contains(r)
				 select r).ToArray());

			Roles.AddUserToRoles(user.Username, groups);

			var ret = GetUser(user.Username);
			ret.Groups = Roles.GetRolesForUser(user.Username);

			return ret;
		}
		#endregion

		#region User Data
		public IDictionary<string, string> RetrieveAllUserData(UserInfo user) {
			if (user == null) throw new ArgumentNullException("user");

			var ret = new Dictionary<string, string>();
			var profile = GetProfile(user.Username);

			foreach(var p in ProfileBase.Properties.OfType<SettingsProperty>()) {
				if (p.PropertyType == typeof(string))
					ret.Add(p.Name, (string)profile[p.Name]);
				else if (profile[p.Name] != null)
					ret.Add(p.Name, profile[p.Name].ToString());
				else
					ret.Add(p.Name, null);

				return ret;
			}

			return ret;
		}

		public string RetrieveUserData(UserInfo user, string key) {
			if (user == null) throw new ArgumentNullException("user");
			if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");

			key = key.ToLowerInvariant();
			var profile = GetProfile(user.Username);
			return profile[key] as string;
		}

		public bool StoreUserData(UserInfo user, string key, string value) {
			if (user == null) throw new ArgumentNullException("user");
			if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");

			try {
				var profile = GetProfile(user.Username);
				profile[key] = value;
				profile.Save();
			}
			catch {
				return false;
			}
			return true;
		}
		#endregion

		#region Internal Utilities
		private MembershipUserInfo ToUserInfo(MembershipUser membershipUser){
			if (membershipUser == null) return null;

			var profile = GetProfile(membershipUser);
			if (profile["DisplayName"] == null)
				profile["DisplayName"] = membershipUser.UserName;
			profile.Save();

			return new MembershipUserInfo(membershipUser.UserName, (string)profile["DisplayName"], membershipUser.Email, membershipUser.IsApproved, membershipUser.CreationDate, this, membershipUser.ProviderUserKey) {
				Groups = Roles.GetRolesForUser(membershipUser.UserName)};
		}

		private static ProfileBase GetProfile(string username) {
			if (String.IsNullOrEmpty(username)) throw new ArgumentNullException("username");

			return ProfileBase.Create(username);
		}

		private static ProfileBase GetProfile(MembershipUser user) {
			if (user == null) throw new ArgumentNullException("user");
			return GetProfile(user.UserName);
		}
		#endregion
	}
}
